import input_output.Open;
import input_output.Save;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.util.Vector;

import javax.swing.JColorChooser;
import javax.swing.JComponent;

import enums.Operation;
import enums.Shape;
import enums.State;
import globals.DrawPadSize;


class DrawPad extends JComponent implements MouseListener, MouseMotionListener {

	//Constants
    private static final Shape INIIIAL_SHAPE = Shape.RECTANGLE;
    private static final Color INITIAL_COLOR = Color.BLACK;
    private static final Color COLOR_BACKGROUND = Color.WHITE;
    private static final Operation INITIAL_OPERATION = Operation.FIRST_OCCURENCE;
    
    //Fields
    private State _state = State.IDLE;
    private Shape _shape = INIIIAL_SHAPE;
    private Color _color = INITIAL_COLOR;
    private Operation _operation = INITIAL_OPERATION;
    
    private Point _start = null; // Where mouse is pressed.
    private Point _end   = null; // Where mouse is dragged to or released.
    
  //holds images for undo operating
    Vector<Image> imageVector = new Vector<Image>();
    
    //BufferedImage stores the underlying saved painting.
    //Initialized first time paintComponent is called.
    private BufferedImage _bufImage = null;
    //Main non-buffered image, temp image
    Image image = null;   
    Image oldImage = null;
    //Overlapping graphics
    Graphics2D graphics2D;
    //BufferedImage for rotating operations
    BufferedImage destinationBI = null;
    
    //Constructor
    public DrawPad() {
    	//should disable lag effect, currently not working
    	setDoubleBuffered(false);
        //Set listeners    
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
    }
    
    //Set shape
    public void setShape(Shape shape) {
        _shape = shape;
    }
    
    //Set color
    public void setColor(Color color) {
        _color = color;
    }
    
    //paintComponent, is called with each repaint()
    @Override public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;  // Downcast to Graphics2D
        //One time initialization of in-memory, saved image.
        if (_bufImage == null && image == null) {
            //This is the first time, initialize _bufImage
            int w = DrawPadSize.getWidth();
            int h = DrawPadSize.getHeight();
            _bufImage = (BufferedImage)this.createImage(w, h);    
            image = createImage(DrawPadSize.getWidth(), DrawPadSize.getHeight());   
            graphics2D = (Graphics2D)image.getGraphics();
			graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            Graphics2D gc = (Graphics2D)_bufImage.getGraphics();
            gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            gc.setColor(COLOR_BACKGROUND);
            gc.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight()); // fill in background
            graphics2D.setColor(COLOR_BACKGROUND);
            graphics2D.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight());
            
        }
        if(_operation == Operation.FIRST_OCCURENCE){
        	//used to save first action to undo menu
        	flip();
        	_operation = Operation.NONE;
        }
        else if(_operation == Operation.RESIZE) {

        	g.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), null);
        	g2.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), null);
        	//scaling oldImage and setting image 
        	image = oldImage.getScaledInstance(DrawPadSize.getWidth(), DrawPadSize.getHeight(), Image.SCALE_SMOOTH);
        	
        	//this part transforms image into BufferedIage to avoid
        	//errors connected with getGraphics using Producer object
        	int w = image.getWidth(null);
            int h = image.getHeight(null);
            int type = BufferedImage.TYPE_INT_RGB;
            BufferedImage dest = new BufferedImage(w, h, type);
            Graphics2D newg2 = dest.createGraphics();
            newg2.drawImage(image, 0, 0, null);
            newg2.dispose();            
            image = dest;
            //end of Image transformation
        	
            //disabling resize mode
            _operation = Operation.NONE;
        }           
        else if(_operation == Operation.ROTATE_LEFT){
        	//Initial operations for image transformation
        	BufferedImage sourceBI;
        	Image inputImage = oldImage;
        	sourceBI = new BufferedImage(inputImage.getWidth(null), inputImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);

        	Graphics2D g2d = (Graphics2D) sourceBI.getGraphics();
        	g2d.drawImage(inputImage, 0, 0, null);

        	AffineTransform at = new AffineTransform();
        	at.rotate(-90.0 * Math.PI / 180.0, sourceBI.getWidth(), sourceBI.getHeight());

        	//translate to make sure the rotation doesn't cut off any image data
        	AffineTransform translationTransform;
        	translationTransform = findTranslation(at, sourceBI);
        	at.preConcatenate(translationTransform);

        	// instantiate and apply affine transformation filter
        	BufferedImageOp bio;
        	bio = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
        	destinationBI = bio.filter(sourceBI, null);
        	g2.drawImage(destinationBI, null, 0, 0);
        	
        	_operation = Operation.NONE;
        	
        	//setting main image as destinationBI is new basic image        	
        	image = destinationBI;
        }
        else if(_operation == Operation.ROTATE_RIGHT){
        	BufferedImage sourceBI;
        	Image inputImage = oldImage;
        	sourceBI = new BufferedImage(inputImage.getWidth(null), inputImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);

        	Graphics2D g2d = (Graphics2D) sourceBI.getGraphics();
        	g2d.drawImage(inputImage, 0, 0, null);

        	AffineTransform at = new AffineTransform();
        	
        	at.rotate(90.0 * Math.PI / 180.0, sourceBI.getWidth(), sourceBI.getHeight());

        	//translate to make sure the rotation doesn't cut off any image data
        	AffineTransform translationTransform;
        	translationTransform = findTranslation(at, sourceBI);
        	at.preConcatenate(translationTransform);

        	// instantiate and apply affine transformation filter
        	BufferedImageOp bio;
        	bio = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
        	destinationBI = bio.filter(sourceBI, null);
        	g2.drawImage(destinationBI, null, 0, 0);
        	
        	_operation = Operation.NONE;
        	
        	//setting main image as destinationBI is new basic image        	
        	image = destinationBI;
        }     
        else if(_operation == Operation.FLIP){
        	BufferedImage sourceBI;
        	Image inputImage = oldImage;
        	sourceBI = new BufferedImage(inputImage.getWidth(null), inputImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);

        	Graphics2D g2d = (Graphics2D) sourceBI.getGraphics();
        	g2d.drawImage(inputImage, 0, 0, null);

        	AffineTransform at = new AffineTransform();
        	
        	at.rotate(180.0 * Math.PI / 180.0, sourceBI.getWidth(), sourceBI.getHeight());

        	//translate to make sure the rotation doesn't cut off any image data
        	AffineTransform translationTransform;
        	translationTransform = findTranslation(at, sourceBI);
        	at.preConcatenate(translationTransform);

        	// instantiate and apply affine transformation filter
        	BufferedImageOp bio;
        	bio = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
        	destinationBI = bio.filter(sourceBI, null);
        	g2.drawImage(destinationBI, null, 0, 0);
        	
        	_operation = Operation.NONE;
        	
        	//setting main image as destinationBI is new basic image        	
        	image = destinationBI;
        }      	
        else if(_operation == Operation.UNDO){
            g.drawImage(oldImage, 0, 0, null);
            graphics2D.drawImage(oldImage, 0, 0, null);
            _operation = Operation.NONE;
        }
        else if(_operation == Operation.REDRAW){
        	g2.drawImage(_bufImage, null, 0, 0);
            g.drawImage(oldImage, 0, 0, null);
            graphics2D.drawImage(oldImage, 0, 0, null);
            _operation = Operation.NONE;
        }
        else{
        	//Display the saved image.
            g2.drawImage(_bufImage, null, 0, 0);
            g.drawImage(image, 0, 0, null);
            if(_operation == Operation.FLIP_HORIZONTALLY){
            	g.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), 0, DrawPadSize.getHeight(), DrawPadSize.getWidth(), 0, null);
            	graphics2D.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), 0, DrawPadSize.getHeight(), DrawPadSize.getWidth(), 0, null);  
            	_operation = Operation.NONE;
            }
            else if(_operation == Operation.FLIP_VERTICALLY){
            	g.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), DrawPadSize.getWidth(), 0, 0, DrawPadSize.getHeight(), null);
            	graphics2D.drawImage(oldImage, 0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight(), DrawPadSize.getWidth(), 0, 0, DrawPadSize.getHeight(), null); 
            	_operation = Operation.NONE;
            }
        }
        
        //Overwrite the screen display with currently dragged image.
        if (_state == State.DRAGGING) {
            //Write shape that is being dragged over the screen image,
            //but not into the saved buffered image.  It will be written
            //on the saved image when the mouse is released.
        	getCurrentShapeCoordinates(g2);
        	getCurrentShapeCoordinates((Graphics2D)g);
        }
    }
    private void getCurrentShapeCoordinates(Graphics2D g2){
    	
    	//temporary values
    	int curStartX = _start.x;
    	int curStartY = _start.y;
    	int curEndX = _end.x;
    	int curEndY = _end.y;
    	
    	//determining if mouse is "moving" in negative coordinates with respect to initial click
    	//if this situation happens, new coordinates for drawing figures are declared
    	if(curEndX < curStartX){
    		if(curEndY < curStartY){
    			drawCurrentShape(g2, curEndX, curEndY, Math.abs(curEndX - curStartX), Math.abs(curEndY - curStartY));
    		}
    		else{
    			drawCurrentShape(g2, curEndX, curStartY, Math.abs(curEndX - curStartX), curEndY - curStartY);
    		}        
    	}
    	else if(curEndY < curStartY){
    		if(curEndX < curStartX){
    			drawCurrentShape(g2, curEndX, curEndY, Math.abs(curEndX - curStartX), Math.abs(curEndY - curStartY));
    		}
    		else{
    			drawCurrentShape(g2, curStartX, curEndY, curEndX - curStartX, Math.abs(curEndY - curStartY));
    		}
    	}
    	else{
    		drawCurrentShape(g2, curStartX, curStartY, curEndX - curStartX, curEndY - curStartY);
    	}
    }
    
    //drawCurrentShape
    private void drawCurrentShape(Graphics2D g2, int x, int y, int width, int height) {
        //Draws current shape on a graphics context, either
        //on the context passed to paintComponent, or the
        //context for the BufferedImage.
    	
        g2.setColor(_color);    // Set the color.

        if(_start.x <= DrawPadSize.getWidth() && _start.y <= DrawPadSize.getHeight()){
        	switch (_shape) {
            case OVAL:
            	g2.drawOval(x, y, width, height);
                break;
            case FILLED_OVAL:
            	g2.fillOval(x, y, width, height);
            	break;
            case RECTANGLE:
                g2.drawRect(x, y, width, height);
                break;    
            case FILLED_RECTANGLE:
            	g2.fillRect(x, y, width, height);
            	break;
            case LINE:
                g2.drawLine(_start.x, _start.y, _end.x, _end.y);
                break;
            case BRUSH:
            	//TODO: gapless implementation of drawing
            	//theoretically it should work with the previous
            	//solution, however because of gaps I decided to use fillOval
            	//to minimize the "lag" effect
				if(g2 != null){
					//g2.fillOval(_end.x, _end.y, 4, 4);
					graphics2D.setColor(_color);
					graphics2D.fillOval(_end.x - 3, _end.y - 3, 5, 5);
				}
				this.repaint();				
				break; 
            case ERASER:

            	g2.setColor(Color.WHITE);
            	g2.fillRect(_end.x - 13, _end.y - 13, 26, 26);
            	g2.setColor(_color);
            	
            	break;
            case FILL:
            	//draw a single pixel
            	g2.drawLine(_end.x, _end.y, _end.x, _end.y);
            	break;
            /* TEST CASE
            case IMAGE:
                System.out.println(width + " " + height);
                g2.drawImage(image, 10, 10, width, height, this);
                break;*/
            default:  // Should never happen!
                g2.drawString("Huh?", 10, 20);
                break;
        	}
        }
    }
    
	//**************************************************
	//Mouse listeners
	//**************************************************
    public void mousePressed(MouseEvent e) {
    	//redraw method checks for changes in image to be stored in vector for undo operation
    	redraw();
        _state = State.DRAGGING;   // Assume we're starting a drag.
        switch(_shape){

    		case ERASER:
        	case BRUSH:
        		_start = e.getPoint();     // Save start point, and also initially
    	        _end   = _start;           // as end point, which drag will change.
    	        this.repaint();
        		break;
        	case LINE:
        	case RECTANGLE:
        	case FILLED_RECTANGLE:
        	case OVAL:
        	case FILLED_OVAL:
        		_start = e.getPoint();     // Save start point, and also initially
    	        _end   = _start;           // as end point, which drag will change.
    	        this.repaint();
        		break;
        	case FILL:
        		/*_end = e.getPoint();
        		_start = e.getPoint();
        		//_color = getPixelColor(_end.x, _end.y);
        	    fillArea(_end.x, _end.y, getPixelColor(_end.x, _end.y));*/
        		break;
        	default:
        		break;
        }
    }
    
    public void mouseDragged(MouseEvent e) {
        _state = State.DRAGGING;   // We're dragging to create a shape.
        switch(_shape){
        	case ERASER:
	        case BRUSH:
	        	_end.x = e.getX();
				_end.y = e.getY();
				this.repaint();
				getCurrentShapeCoordinates((Graphics2D)image.getGraphics());
	    		break;
	    	case LINE:
	    	case RECTANGLE:
	    	case FILLED_RECTANGLE:
	    	case OVAL:
	    	case FILLED_OVAL:
	    		_end = e.getPoint();       // Set end point of drag.  May change.
	            this.repaint();            // After change, show new shape
	    		break;
	    	case FILL:
	    		break;
	    	default:
	    		break;
        }
    }
    
    public void mouseReleased(MouseEvent e) {
        //... If released at end of drag, write shape into the BufferedImage,
        //    which saves it in the drawing.
    	_end = e.getPoint();      // Set end point of drag.
    	switch(_shape){
    		case ERASER:
	    	case BRUSH:
	        	break;
	    	case LINE:
	    	case RECTANGLE:
	    	case FILLED_RECTANGLE:
	    	case OVAL:
	    	case FILLED_OVAL:
	    		if (_state == State.DRAGGING) {
		            _state = State.IDLE;
		            //... Draw current shape in saved buffered image.
		            getCurrentShapeCoordinates((Graphics2D)image.getGraphics());
		            this.repaint();
	    		}
	    		break;
	    	case FILL:
	    		break;
	    	default:
	    		break;
    	}
    }
    
    public void mouseClicked(MouseEvent e) {
    	switch(_shape){
    		case FILL:
    			_end = e.getPoint();
        		_start = e.getPoint();
        		//_color = getPixelColor(_end.x, _end.y);
        	    //fillArea(_end.x, _end.y, getPixelColor(_end.x, _end.y), _color);
    	}
    }
    
  //redraw method checks for changes in image to be stored in vector for undo operation
    
    //ignored mouse listeners
    public void mouseMoved  (MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited (MouseEvent e) {}
    
  //this is the clear
	//it sets the colors as white
	//then it fills the window with white
	//thin it sets the color back to black
    /* currently disabled
	public void clear(){
		graphics2D.setPaint(Color.WHITE);
		graphics2D.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight());
		graphics2D.setPaint(_color);
		repaint();
	}*/
	
  //fill algorithm
	public void fillArea(int x, int y, Color replacedColor, Color targetColor){
			  if(getPixelColor(x,y).equals(Color.WHITE)){
				  //setPixel(x,y, clickColor);
				  //graphics2D.drawLine(x, y, x, y);
				  _end.x = x;
				  _end.y = y;
				  while(x < DrawPadSize.getWidth() && y < DrawPadSize.getHeight()){
					  getCurrentShapeCoordinates((Graphics2D)image.getGraphics());
					  repaint();
					  fillArea(x+1,y, replacedColor, targetColor);
					  fillArea(x-1,y, replacedColor, targetColor);
					  fillArea(x,y+1, replacedColor, targetColor);
					  fillArea(x,y-1, replacedColor, targetColor);
				  }
			}
	}
	//opens JColorChooser and set's current color to the chosen one
	public void getColor(){
		Color toolColor = JColorChooser.showDialog(this, "Choose Color", graphics2D.getColor());
		if (toolColor != null){
			_color =  toolColor;
		}		
		repaint();
	}
	
	//getting pixel color from BufferedImage
	public Color getPixelColor(int x, int y){
		int clickColorRGB = _bufImage.getRGB(x, y);
		int  red   = (clickColorRGB & 0x00ff0000) >> 16;
	    int  green = (clickColorRGB & 0x0000ff00) >> 8;
	    int  blue  =  clickColorRGB & 0x000000ff;
	    
	    return new Color(red, green, blue);
	}
	
	public void saveImage(){
		Save saveFile = new Save(image, "bmp");
		saveFile.saveImage();
	}
	
	public void openImage(){
		Open openFile = new Open("bmp");
		Image newImage = openFile.openImage();
		if(newImage != null){
			oldImage = newImage;
			DrawPadSize.setWidth(oldImage.getWidth(null));
			DrawPadSize.setHeight(oldImage.getHeight(null));
			refreshPanel();
			_operation = Operation.RESIZE;
		}
		
	}
	
	public void setNewSize(){
		ImageOperation opImage = new ImageOperation();
		opImage.setSize();
		//add "previous" image to vector container
		imageVector.add(image);
		refreshPanel();		
	}
	
	//Resize current image
	public void setResize(){
		//Holding current image as it will be reseted
		oldImage = image;
		//calling Option Box to get new size of image
		//add "previous image to vector container
		imageVector.add(image);
		ImageOperation opImage = new ImageOperation();
		opImage.setSize();	
		//Refreshing draw panel as there is new size
		refreshPanel();
		//setting resize flag to true to inform paintComponent how to redraw image
		_operation = Operation.RESIZE;
	}
	
	public void rotateLeft(){

		oldImage = image;
		DrawPadSize.setWidth(oldImage.getHeight(null));
		DrawPadSize.setHeight(oldImage.getWidth(null));
		//setting resize flag to true to inform paintComponent how to redraw image
		//add "previous image to vector container
		imageVector.add(image);
		refreshPanel();
		_operation = Operation.ROTATE_LEFT;
	}
	
	public void rotateRight(){

		oldImage = image;
		DrawPadSize.setWidth(oldImage.getHeight(null));
		DrawPadSize.setHeight(oldImage.getWidth(null));
		//add "previous image to vector container
		imageVector.add(image);
		refreshPanel();
		_operation = Operation.ROTATE_RIGHT;
	}
	
	public void flip(){
		oldImage = image;
		//add "previous image to vector container
		imageVector.add(image);
		refreshPanel();
		_operation = Operation.FLIP;
	}
	
	public void flipHorizontally(){
		oldImage = image;
		//add "previous image to vector container
		imageVector.add(image);
		refreshPanel();
		_operation = Operation.FLIP_HORIZONTALLY;
	}
	
	public void flipVertically(){
		oldImage = image;
		//add "previous image to vector container
		imageVector.add(image);
		refreshPanel();
		_operation = Operation.FLIP_VERTICALLY;
	}
	
	public void redraw(){
		oldImage = image;
		if(!imageVector.isEmpty()){
			if(!(imageVector.lastElement().toString().equals(image.toString()))){
				imageVector.add(image);
			}
			refreshPanel();
			_operation = Operation.REDRAW;
		}
		
	}
	public void undo(){
		if(!imageVector.isEmpty()){
			oldImage = imageVector.lastElement();
			
			//don't allow vector to be empty
			if(!(imageVector.size() < 2)){
				imageVector.remove(imageVector.lastElement());
			}
				DrawPadSize.setWidth(oldImage.getWidth(null));
				DrawPadSize.setHeight(oldImage.getHeight(null));
				refreshPanel();
				_operation = Operation.UNDO;
		}
	}
	
	//Used when painting area is resized
	//Layout has to change its 
	private void refreshPanel(){
		Painter.setLayout();
		int w = DrawPadSize.getWidth();//this.getWidth();
        int h = DrawPadSize.getHeight();//this.getHeight();
        _bufImage = (BufferedImage)this.createImage(w, h);
        image = createImage(DrawPadSize.getWidth(), DrawPadSize.getHeight());        
        graphics2D = (Graphics2D)image.getGraphics();
		graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Graphics2D gc = (Graphics2D)_bufImage.getGraphics();
        gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        gc.setColor(COLOR_BACKGROUND);
        gc.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight()); // fill in background
        graphics2D.setColor(COLOR_BACKGROUND);
        graphics2D.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight());
        graphics2D.setPaint(Color.WHITE);
		graphics2D.fillRect(0, 0, DrawPadSize.getWidth(), DrawPadSize.getHeight());
		graphics2D.setPaint(_color);
		this.repaint();
	}
	
	//get AffineTransform for rotating operation
	private AffineTransform findTranslation(AffineTransform at, BufferedImage bi) {
	    Point2D p2din, p2dout;

	    p2din = new Point2D.Double(0.0, 0.0);
	    p2dout = at.transform(p2din, null);
	    double ytrans = p2dout.getY();
	    p2din = new Point2D.Double(0, bi.getHeight());
	    p2dout = at.transform(p2din, null);
	    double xtrans = p2dout.getX();

	    AffineTransform tat = new AffineTransform();
	    
	    //rotation degree less than 90
	    if(_operation == Operation.ROTATE_RIGHT){
		    tat.translate(-xtrans, -ytrans);
	    }
	    //if rotation degree is greater than 90
	    else if(_operation == Operation.ROTATE_LEFT || _operation == Operation.FLIP){
	    	tat.translate(-xtrans + DrawPadSize.getWidth(), -ytrans + DrawPadSize.getHeight());
	    }
	    return tat;
	  }
}